package eecs.net.xmpp;
//Class: EECS 285
//Project: 4 (Virtual Markerboard)
//Due Date: Monday December 6th, 2010
//

import eecs.net.xmpp.EECSPacketListener;
import eecs.net.xmpp.DataListener.CallType;
import eecs.util.MException;

import java.io.IOException;

import org.jivesoftware.smack.*;
//import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
//import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.packet.Message;
//import org.jivesoftware.smack.packet.Packet;
//import org.jivesoftware.smack.packet.PacketExtension;
//import org.jivesoftware.smack.packet.Presence;
//import org.jivesoftware.smack.packet.Presence.Type;

import java.util.Vector;
import java.util.Map;
import java.util.TreeMap;
//import java.util.Set;
//import java.util.SortedSet;
//import sun.reflect.*;
import static eecs.util.Utility.*;


// ---------------------------------------------------------------------------

/**
 * This class provides incoming and outgoing communication between clients who
 * share the same session ID. Communication is done over the XMPP chat protocol
 * using XML chat packets to push data to other clietns. Clients are able to
 * send and receive POJO's (Plain Old Java Objects) very easily.
 * 
 * @author Vidal Borromeo
 */
public class EECSXMPPConnection
{
  public static final boolean DEBUG_MODE = true;

  public static final String ID_SEPARATOR = "__";
  private String sessionID = null;
  private Roster roster = null;
  private final String sendingAccount = "eecsclient@gmail.com";
  private final String sendingAccountPwd = "flowbot$";
  private final String receivingAccount = "eecsreceiver@gmail.com";
  private final String receivingAccountPwd = "flowbot$";


  XMPPConnection outputConnection = new XMPPConnection(
      new ConnectionConfiguration("talk.google.com", 5222, "appspot.com"));
  XMPPConnection inputConnection = new XMPPConnection(
      new ConnectionConfiguration("talk.google.com", 5222, "appspot.com"));


  private EECSPacketListener myPacketListener = null;
  private DataListener dataListener = null;
  
  @SuppressWarnings("unused")
  private String userID = null;

  private DataListenerAsyncCall outgoingCall = null;


  // Collection of packet IDs that were already sent. See didISendThisPacket().
  private Vector<String> sentPacketIDs = new Vector<String>();

  // ---------------------------------------------------------------------------

  /**
   * EECS XMPP connection for connecting to other clients via the XMPP protocol.
   * Use this for:
   * <ul>
   * <li>Outgoing Client Traffic: traffic generated by this client to be
   * broadcast to other clients.
   * <li>Incoming Client Traffic: traffic broadcast by other clients and
   * listened for here.
   * </ul>
   * <p/>
   * 
   * @author Vidal Borromeo
   */
  public EECSXMPPConnection()
  {

  }// ctor

  // ---------------------------------------------------------------------------

  /**
   * Connects to the XMPP server.
   * <p/>
   * 
   * @param sessionID_
   *          the unique ID for the session that this client (and others) will
   *          be sharing.
   * @param userID_
   *          the unique user name for this user.
   * @param listener_
   *          the listener object
   * @throws IOException
   *           For any problems connecting. Specific message provided if
   *           available.
   * @author Vidal Borromeo
   */
  public void connect(String sessionID_, String userID_, DataListener listener_)
      throws MException
  {
    // Abort if already connected.
    if( isConnected() )
    {
      debug("Already connected. Aborting connection attempt.");
      return;
    }

    // Check for something as session ID.
    if( sessionID_ == null || sessionID_.equals("") )
      throw new MException("sessionID is null or empty");

    sessionID = sessionID_;
    userID = userID_;
    dataListener = listener_;
    myPacketListener = new EECSPacketListener(this, listener_);
    outgoingCall = new DataListenerAsyncCall(dataListener,
        CallType.OUTGOING_SEMAPHORE, null);


    debug("Connecting...");
    try
    {
      outputConnection.connect();
      inputConnection.connect();
      debug("Outgoing: Connected to " + outputConnection.getHost());
      debug("Incoming: Connected to " + inputConnection.getHost());
    }
    catch( Exception e )
    // catch( XMPPException e )
    {
      String msg = "";
      if( !outputConnection.isConnected() )
        msg = "Could not connect to " + outputConnection.getHost() + " for Outgoing connection.";
      else if( !inputConnection.isConnected() )
        msg = "Could not connect to " + inputConnection.getHost() + " for Incoming connection.";

      //debug(msg);
      
      // Disconnect Both to ensure consistent state.
      inputConnection.disconnect();
      outputConnection.disconnect();

      throw new MException(msg);
      //return;

    }// 
    
    
    //assert isConnected();
    
    
    // Accept only messages
    PacketFilter filter = new AndFilter(new PacketTypeFilter(Message.class));

    // Register the listener.
    // outputConnection.addPacketListener(myPacketListener, filter);
    inputConnection.addPacketListener(myPacketListener, filter);

    // Now that connection is established, Login.
    try
    {
      debug("Logging in");
      outputConnection.login(sendingAccount, sendingAccountPwd);
      inputConnection.login(receivingAccount, receivingAccountPwd);
      debug("Incoming: Logged in as " + inputConnection.getUser());
      debug("Outgoing: Logged in as " + outputConnection.getUser());

      // Send 'presence'. Here as development aid. Not used in program.
      //Presence presence = new Presence(Presence.Type.available);
      //outputConnection.sendPacket(presence);
      //inputConnection.sendPacket(presence);

      // Make sure accounts can talk to each other.
      // Duplicates are automatically ignored.
      roster = outputConnection.getRoster();
      if( roster != null )
      {
        roster.createEntry(receivingAccount, null, null);
        roster.createEntry(sendingAccount, null, null);
        // debug(roster.getEntries().toString());
      }

      roster = inputConnection.getRoster();
      if( roster != null )
      {
        roster.createEntry(receivingAccount, null, null);
        roster.createEntry(sendingAccount, null, null);
        // debug(roster.getEntries().toString());
      }

    }// try
    catch( Exception e )
    // catch( XMPPException e )
    {
      
      String msg = "Error while connecting.";
      if( !outputConnection.isAuthenticated() )
      {
        msg = "Could not logon to outgoing connection.";
        debug("Outgoing: Failed to log in as " + outputConnection.getUser());
      }
      else if( !inputConnection.isAuthenticated() )
      {
        msg = "Could not logon to incoming connection.";
        debug("Incoming: Failed to log in as " + inputConnection.getUser());
      }

      // Disconnect Both to ensure consistent state.
      inputConnection.disconnect();
      outputConnection.disconnect();

      throw new MException(msg);
    }// catch

  }// connect

  // ---------------------------------------------------------------------------

  /**
   * Disconnects from the XMPP server.
   * <p/>
   * 
   * @return <code>true</code>, disconnect was successful; <code>false</code>,
   *         disconnect was not successful.
   * @author Vidal Borromeo
   */
  public boolean disconnect()
  {
    debug("Disconnecting...");
    try
    {
      inputConnection.disconnect();
      outputConnection.disconnect();
      debug("Disconnect successful.");
    }
    catch( Exception e )
    {
      debug("Disconnect failed.");
      return false;
    }
    return true;
  }// disconnect

  // ---------------------------------------------------------------------------

  public boolean isConnected()
  {
    return inputConnection.isConnected() && outputConnection.isConnected();
  }// isConnected

  // ---------------------------------------------------------------------------

  /**
   * Sends the given map of properties to other clients. Only clients sharing
   * this session will process the broadcast. The client must have a
   * DataListener with this class.
   * <p/>
   * @param map
   *          java.util.Map<String, Object> map of property keys to Java Objects
   *          to send
   * @throws IOException
   * @author Vidal Borromeo
   */
  public void broadcast(final Map<String, Object> map) throws IOException
  {
    if( !outputConnection.isConnected() )
      throw new IOException("Not Connected");

    // Notify listener of outgoing packet.
    if( dataListener != null )
    {
      //dataListener.outgoingSemaphore();
      (new Thread(outgoingCall)).start();
    }
    
    Message msg;
    try
    {
      msg = new Message(receivingAccount, Message.Type.chat);

      // Log original packetID, then prepend the sessionID.
      logPacketID(msg.getPacketID());
      msg.setPacketID(sessionID + ID_SEPARATOR + msg.getPacketID());
      debug("Set packetID to:" + msg.getPacketID());

      for (String key : map.keySet())
      {
        debug("Setting message property: " + key);
        msg.setProperty(key, map.get(key));
      }
      // debug("Full XML:");
      // debug(msg.toXML());

      debug("Sending message. MESSAGE SIZE: " + msg.toXML().length());
      outputConnection.sendPacket(msg);
      debug("Message sent.");
    }
    catch( Exception e )
    {
      debug("Failed to send message");
      stackTrace(e);
      throw new IOException("Failed to send message");
    }
  }// broadcast(map)

  // ---------------------------------------------------------------------------

  /**
   * Sends the given parameter to other clients. Only clients sharing this
   * session will process the broadcast. The client must have a callback
   * registered for the given property name.
   * <p/>
   * 
   * @param property_name
   *          the String name that other clients will recognize for handling
   *          this Object
   * @param obj
   *          the Java Object that is being sent to other clients in this
   *          session.
   * @throws IOException
   * @author Vidal Borromeo
   */
  @SuppressWarnings("serial")
  public void broadcast(final String property_name, final Object obj)
      throws IOException
  {
    if( !outputConnection.isConnected() )
      throw new IOException("Not Connected");
    else if( property_name == null )
      throw new IOException("Null Property Key");
    else if( obj == null )
      throw new IOException("Null Property Value");

    broadcast(new TreeMap<String, Object>()
    {
      {
        put(property_name, obj);
      }
    });

  }// broadcast(key, value)

  // ---------------------------------------------------------------------------

  /**
   * Sends the given parameters to other clients. Only clients sharing this
   * session will process the broadcast. The client must have a callback
   * registered for the given property name.
   * <p/>
   * 
   * @param key1
   *          the String name that other clients will recognize for handling
   *          this Object
   * @param obj1
   *          the Java Object that is being sent to other clients in this
   *          session.
   * @param key2
   *          the String name that other clients will recognize for handling
   *          this Object
   * @param obj2
   *          the Java Object that is being sent to other clients in this
   *          session.
   * @throws IOException
   * @author Vidal Borromeo
   */
  @SuppressWarnings("serial")
  public void broadcast(final String key1, final Object obj1,
      final String key2, final Object obj2) throws IOException
  {
    if( !outputConnection.isConnected() )
      throw new IOException("Not Connected");
    else if( key1 == null )
      throw new IOException("Null Property Key");
    else if( obj1 == null )
      throw new IOException("Null Property Value");
    else if( key2 == null )
      throw new IOException("Null Property Key");
    else if( obj2 == null )
      throw new IOException("Null Property Value");

    broadcast(new TreeMap<String, Object>()
    {
      {
        put(key1, obj1);
        put(key2, obj2);
      }
    });

  }// broadcast(key, value, key, value)

  // ---------------------------------------------------------------------------

  /**
   * Generates an ID. Use this for cases that need almost-unique identifiers.
   * <p/>
   * 
   * @return A "reasonably rare" string ID.
   * 
   * @author Vidal Borromeo
   */
  public static String generateID()
  {
    return Long.toHexString(Double.doubleToLongBits(Math.random()))
        .substring(3, 10).toUpperCase();
    // return UUID.randomUUID().toString();
    // return (new Message()).getPacketID();
  }// generateID

  // ---------------------------------------------------------------------------

  /**
   * Logs the given packetID as having come from this client.
   * <p/>
   * 
   * @param packetID_
   *          the original packetID assigned to the packet. Note:
   *          EECSXMPPConnection reassigns a new packet ID of the form:
   *          sessionID_originalPacketID This method expects just the
   *          originalPacketID portion.
   * @author Vidal Borromeo
   */
  public void logPacketID(String packetID_)
  {
    synchronized( sentPacketIDs )
    {
      debug("Comparing packet ID: " + packetID_);
      debug(sentPacketIDs.toString());

      if( sentPacketIDs.size() > 6 )
      {
        debug("Trimming sentPacketIDs list.");
        for (int i = 0; i < 3; i++)
        {
          sentPacketIDs.remove(0); // keep removing the first element.
        }

      }// if
      sentPacketIDs.add(packetID_);
    }// sync
  }// logPacketID

  // ---------------------------------------------------------------------------

  /**
   * Checks to see if a packetID was sent from this client.
   * <p/>
   * 
   * @param packetID_
   *          the original packetID assigned to the packet. Note:
   *          EECSXMPPConnection reassigns a new packet ID of the form:
   *          sessionID_originalPacketID This method expects just the
   *          originalPacketID portion.
   * @author Vidal Borromeo
   */
  public boolean didISendThisPacket(String packetID_)
  {
    debug("Comparing packet IDs: " + packetID_);
    synchronized( sentPacketIDs )
    {
      debug(sentPacketIDs.toString());
      return sentPacketIDs.contains(packetID_);
    }
  }// didISendThisPacket

  // ---------------------------------------------------------------------------

  /**
   * Checks to see if a packetID was sent from this client.
   * <p/>
   * 
   * @param sessionID_
   *          the sessionID assigned to the packet. Note: EECSXMPPConnection
   *          reassigns a new packet ID of the form: sessionID_originalPacketID
   *          This method expects just the sessionID portion.
   * @author Vidal Borromeo
   */
  public boolean isThisMySession(String sessionID_)
  {
    return sessionID.equals(sessionID_);
  }

}// EECSXMPPConnection
